﻿using Microsoft.Media.ISO;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#if NETFX_CORE
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Web.Http;
#else
using System.Net;
#endif

namespace Microsoft.Media.IngestClient
{
    /// <summary>
    /// Respresents a class capable of sending properly formatted PIFF data to an AMS ingest service, exposing information about the session, and handling retrying, failures and cancellation.
    /// </summary>
    public sealed class LiveClient : IDisposable
    {
        readonly object syncLock = new object();
        PushClient pushClient;
        CancellationTokenSource cts;
        Queue<PiffFragment> pushedFragments;
        Task pushTask;
        TaskCompletionSource<object> retryTaskCompletionSource;
        PiffHeader header;
        uint fragmentIndex;
        IPushOutputConsumer outputConsumer;

        /// <summary>
        /// Raised when the internet connection fails and the client attempts a retry. Event args allow retry to be cancelled and include error that necessitated retry.
        /// </summary>
        public event EventHandler<PushRetryEventArgs> PushRetry;

        /// <summary>
        /// Raised when the internet connection has failed and the client is done retrying. Event args include error that caused failure.
        /// </summary>
        public event EventHandler<PushFailureEventArgs> PushFailed;

        /// <summary>
        /// Raised for each MPEG4 box that has been successfully pushed to the server.
        /// </summary>
        public event EventHandler<BoxPushedEventArgs> BoxPushed;

        /// <summary>
        /// Raised when a fragment has been successfully sent to the server.
        /// </summary>
        public event EventHandler<FragmentDeliveryEventArgs> FragmentSent;

        /// <summary>
        /// Raised when a new fragment has been added to the queued to send to the server.
        /// </summary>
        public event EventHandler<FragmentDeliveryEventArgs> FragmentQueued;

        /// <summary>
        /// Creates a new instance of LiveClient
        /// </summary>
        public LiveClient()
        {
            this.cts = new CancellationTokenSource();
            // the combination of MaxRetries & RetryDelay will give us a minimum retry time of 5 min; longer if each failure takes a while
            MaxRetries = 100;
            RetryDelay = TimeSpan.FromSeconds(3);
        }

        /// <summary>
        /// Gets or sets the maximum number of retries in the event of an internet failure.
        /// </summary>
        public int? MaxRetries { get; set; }

        /// <summary>
        /// Gets or sets the amount of time to wait between retries in the event of an internet failure.
        /// </summary>
        public TimeSpan? RetryDelay { get; set; }

        /// <summary>
        /// Gets the total bytes that have been provided to this object. Note: bytes may still be in buffer.
        /// </summary>
        public ulong BytesPushed { get { return pushClient != null ? pushClient.BytesPushed : 0; } }

        /// <summary>
        /// Gets the total bytes read by the output consumer. This should be the same as the number of bytes attempted to send to the server.
        /// </summary>
        public ulong BytesRead { get { return pushClient != null ? pushClient.BytesRead : 0; } }

        /// <summary>
        /// Gets the total bytes successfully sent by the output consumer.
        /// </summary>
        public ulong BytesSent { get { return pushClient != null ? pushClient.BytesSent : 0; } }

        /// <summary>
        /// Gets the last Http response code from the PushClient. If not applicable or success, Ok is returned.
        /// </summary>
        public HttpStatusCode HttpResponseStatusCode
        {
            get
            {
                return outputConsumer is HttpPushOutputConsumer 
                    ? ((HttpPushOutputConsumer)outputConsumer).HttpResponseStatusCode 
#if NETFX_CORE
                    : HttpStatusCode.Ok;
#else
                    : HttpStatusCode.OK;
#endif
            }
        }

#if NETFX_CORE
        
        /// <summary>
        /// Initializes the session and ensures that the live ingest service is capable of receiving information.
        /// </summary>
        /// <param name="ingestionUrl">The live ingest channel url</param>
        /// <param name="streamId">The name of the stream (this will be appended to the url). Note: this can be set to anything.</param>
        /// <returns>An awaitable action that returns when information is ready to be sent.</returns>
        public IAsyncAction InitializeAsync(Uri ingestionUrl, string streamId)
        {
            return AsyncInfo.Run(c => InitializeAsync(ingestionUrl, streamId, c));
        }

        async Task InitializeAsync(Uri ingestionUrl, string streamId, CancellationToken c)
        {
            using (var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10))) // timeout of 10 sec according to spec
            {
                using (var cts2 = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, c, timeoutCts.Token))
                {
                    var httpOutputConsumer = new HttpPushOutputConsumer()
                    {
                        IngestionUrl = ingestionUrl,
                        StreamIdentifier = streamId
                    };
                    outputConsumer = httpOutputConsumer;
                    await httpOutputConsumer.InitializeAsync().AsTask(cts2.Token);
                }
            }
        }
#else
        /// <summary>
        /// Initializes the session and ensures that the live ingest service is capable of receiving information.
        /// </summary>
        /// <param name="ingestionUrl">The live ingest channel url</param>
        /// <param name="streamId">The name of the stream (this will be appended to the url). Note: this can be set to anything.</param>
        /// <param name="c">A cancellation token that can be used to cancel the operation.</param>
        /// <returns>An awaitable action that returns when information is ready to be sent.</returns>
        public async Task InitializeAsync(Uri ingestionUrl, string streamId, CancellationToken c)
        {
            using (var cts2 = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, c))
            {
                var httpOutputConsumer = new HttpPushOutputConsumer()
                {
                    IngestionUrl = ingestionUrl,
                    StreamIdentifier = streamId
                };
                outputConsumer = httpOutputConsumer;
                await httpOutputConsumer.InitializeAsync(cts2.Token);
            }
        }
#endif

        /// <summary>
        /// Used to initialize the session when using a custom IPushOutputConsumer implementation.
        /// </summary>
        /// <param name="outputConsumer">A custom IPushOutputConsumer implementation to send data to.</param>
        public void Initialize(IPushOutputConsumer outputConsumer)
        {
            this.outputConsumer = outputConsumer;
        }

#if NETFX_CORE
        /// <summary>
        /// Opens the connection to the server and start by sending the PIFF header. Note: this is not thread safe and the Start method should be used instead when calling from a background thread.
        /// </summary>
        /// <param name="header">The PIFF header (ftyp, uuid, and moov boxes)</param>
        /// <returns>An awaitable action that returns when the data has been queued.</returns>
        public IAsyncAction StartAsync(PiffHeader header)
        {
            return AsyncInfo.Run(c => StartAsync(header, c));
        }

        async Task StartAsync(PiffHeader header, CancellationToken c)
#else
        /// <summary>
        /// Opens the connection to the server and start by sending the PIFF header. Note: this is not thread safe and the Start method should be used instead when calling from a background thread.
        /// </summary>
        /// <param name="header">The PIFF header (ftyp, uuid, and moov boxes)</param>
        /// <param name="c">A cancellation token that can be used to cancel the operation.</param>
        /// <returns>An awaitable action that returns when the data has been queued.</returns>
        public async Task StartAsync(PiffHeader header, CancellationToken c)
#endif
        {
            using (var cts2 = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, c))
            {
                try
                {
                    this.pushedFragments = new Queue<PiffFragment>();
                    this.pushClient = new PushClient();
                    fragmentIndex = 0;
#if NETFX_CORE
                    await pushClient.PushBoxesAsync(new IBox[] { header.FileType, header.LiveServerManifest, header.Movie }, null).AsTask(cts2.Token);
#else
                    await pushClient.PushBoxesAsync(new IBox[] { header.FileType, header.LiveServerManifest, header.Movie }, null, cts2.Token);
#endif

                    this.header = header;

                    // pushTask will push until we're done sending everything (including retrys).
                    this.pushTask = PushAsync(outputConsumer)
                        .ContinueWith(async t =>
                        {
                            await outputConsumer.TerminateAsync();
                            this.pushedFragments = null;
                            this.pushClient.Dispose();
                            this.pushClient = null;
                        }, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
                }
                catch
                {
                    this.pushedFragments = null;
                    if (this.pushClient != null) this.pushClient.Dispose();
                    this.pushClient = null;
                    throw;
                }
            }
        }

        /// <summary>
        /// Opens the connection to the server and start by sending the PIFF header. Note: this is thread safe and should be used when calling from a background thread (as opposed to StartAsync).
        /// </summary>
        /// <param name="header">The PIFF header (ftyp, uuid, and moov boxes)</param>
        public void Start(PiffHeader header)
        {
            lock (syncLock)
            {
                try
                {
                    this.pushedFragments = new Queue<PiffFragment>();
                    this.pushClient = new PushClient();
                    fragmentIndex = 0;
                    pushClient.PushBoxes(new IBox[] { header.FileType, header.LiveServerManifest, header.Movie }, null);

                    this.header = header;

                    // pushTask will push until we're done sending everything (including retrys).
                    this.pushTask = Push(outputConsumer)
                        .ContinueWith(async t =>
                        {
                            await outputConsumer.TerminateAsync();
                            this.pushedFragments = null;
                            this.pushClient.Dispose();
                            this.pushClient = null;
                        }).Unwrap();
                }
                catch
                {
                    this.pushedFragments = null;
                    if (this.pushClient != null) this.pushClient.Dispose();
                    this.pushClient = null;
                    throw;
                }
            }
        }

        async Task PushAsync(IPushOutputConsumer outputConsumer)
        {
            // do not await the send operation
            var progress = new Progress<BoxHistory>();
            var retryQueueCapacity = 2 * header.Movie.Tracks.Count(); // the last 2 fragments for each track
            var retryQueue = new List<PiffFragment>(retryQueueCapacity);
#if NETFX_CORE
            EventHandler<BoxHistory> progressChangedHandler = (s, e) =>
#else
            ProgressEventHandler<BoxHistory> progressChangedHandler = (s, e) =>
#endif
            {
                if (BoxPushed != null) BoxPushed(this, new BoxPushedEventArgs(e.Box));
                if (e.Box is MediaDataBox) // only check for MediaDataBox since it is the last box sent in a fragment.
                {
                    var fragment = pushedFragments.Dequeue();
                    var purpose = e.UserState == null ? FragmentDeliveryPurpose.Normal : FragmentDeliveryPurpose.Retry;
                    if (FragmentSent != null) FragmentSent(this, new FragmentDeliveryEventArgs(fragment, purpose));
                    if (purpose != FragmentDeliveryPurpose.Retry) // retry fragments are already in the retryQueue
                    {
                        var fragmentsFromSameTrack = retryQueue.Where(f => f.MovieFragment.TrackFragment.TrackFragmentHeader.TrackId == fragment.MovieFragment.TrackFragment.TrackFragmentHeader.TrackId).ToList();
                        if (fragmentsFromSameTrack.Count > 1) retryQueue.Remove(fragmentsFromSameTrack.First()); // we only need to store the last 2 fragments per track. Remove everything else + keep in mind that we're about to add a new fragment immediately.
                        retryQueue.Add(fragment); // always store the last 2 fragments sent in case a retry is needed
                    }
                }
            };
            progress.ProgressChanged += progressChangedHandler;
            try
            {
                // start pushing until we're done sending everything but return immediately from this method.
#if NETFX_CORE
                var sendTask = pushClient.SendBoxesAsync(outputConsumer).AsTask(cts.Token, progress);
#else
                var sendTask = pushClient.SendBoxesAsync(outputConsumer, cts.Token, progress);
#endif

                await RunAndRetryAsync(sendTask, async error =>
                {
                    var args = new PushRetryEventArgs(error.Message);
                    if (PushRetry != null) PushRetry(this, args);
                    if (args.Cancel) throw new OperationCanceledException();

                    // get a copy of last 2 successfully sent fragments + any that were in the process of sending.
                    var fragmentsToResend = retryQueue.ToList();
                    var queuedFragments = pushedFragments.Except(fragmentsToResend).ToList();
                    this.pushedFragments.Clear();
                    this.pushClient.Dispose();
                    this.pushClient = new PushClient();
                    var retryToken = new object();

                    retryTaskCompletionSource = new TaskCompletionSource<object>();
                    try
                    {
                        // resend the header boxes
#if NETFX_CORE
                        await pushClient.PushBoxesAsync(new IBox[] { header.FileType, header.LiveServerManifest, header.Movie }, retryToken).AsTask(cts.Token);
#else
                        await pushClient.PushBoxesAsync(new IBox[] { header.FileType, header.LiveServerManifest, header.Movie }, retryToken, cts.Token);
#endif
                        // resend the last 2 fragments
                        foreach (var fragment in fragmentsToResend)
                        {
                            pushedFragments.Enqueue(fragment);
#if NETFX_CORE
                            await pushClient.PushBoxesAsync(new IBox[] { fragment.MovieFragment, fragment.MediaData }, retryToken).AsTask(cts.Token);
#else
                            await pushClient.PushBoxesAsync(new IBox[] { fragment.MovieFragment, fragment.MediaData }, retryToken, cts.Token);
#endif
                        }
                        // send any that were in the queue
                        foreach (var fragment in queuedFragments)
                        {
                            pushedFragments.Enqueue(fragment);
#if NETFX_CORE
                            await pushClient.PushBoxesAsync(new IBox[] { fragment.MovieFragment, fragment.MediaData }, null).AsTask(cts.Token);
#else
                            await pushClient.PushBoxesAsync(new IBox[] { fragment.MovieFragment, fragment.MediaData }, null, cts.Token);
#endif
                        }
                        retryTaskCompletionSource.SetResult(null);
                    }
                    catch (OperationCanceledException)
                    {
                        retryTaskCompletionSource.SetCanceled();
                        throw;
                    }
                    catch (Exception ex)
                    {
                        retryTaskCompletionSource.SetException(ex);
                        throw;
                    }
                    finally
                    {
                        retryTaskCompletionSource = null;
                    }

#if NETFX_CORE
                    await pushClient.SendBoxesAsync(outputConsumer).AsTask(cts.Token, progress);
#else
                    await pushClient.SendBoxesAsync(outputConsumer, cts.Token, progress);
#endif
                }, MaxRetries, RetryDelay);
            }
            catch (OperationCanceledException) { throw; }
            catch (Exception ex)
            {
                if (PushFailed != null) PushFailed(this, new PushFailureEventArgs(ex.Message));
                throw;
            }
            finally
            {
                progress.ProgressChanged -= progressChangedHandler;
            }
        }

        Task Push(IPushOutputConsumer outputConsumer)
        {
            var progress = new Progress<BoxHistory>();
            var retryQueueCapacity = 2 * header.Movie.Tracks.Count(); // the last 2 fragments for each track
            var retryQueue = new List<PiffFragment>(retryQueueCapacity);
#if NETFX_CORE
            EventHandler<BoxHistory> progressChangedHandler = (s, e) =>
#else
            ProgressEventHandler<BoxHistory> progressChangedHandler = (s, e) =>
#endif
            {
                lock (syncLock)
                {
                    if (BoxPushed != null) BoxPushed(this, new BoxPushedEventArgs(e.Box));
                    if (e.Box is MediaDataBox) // only check for MediaDataBox since it is the last box sent in a fragment.
                    {
                        var fragment = pushedFragments.Dequeue();
                        var purpose = e.UserState == null ? FragmentDeliveryPurpose.Normal : FragmentDeliveryPurpose.Retry;
                        if (FragmentSent != null) FragmentSent(this, new FragmentDeliveryEventArgs(fragment, purpose));
                        if (purpose != FragmentDeliveryPurpose.Retry) // retry fragments are already in the retryQueue
                        {
                            var fragmentsFromSameTrack = retryQueue.Where(f => f.MovieFragment.TrackFragment.TrackFragmentHeader.TrackId == fragment.MovieFragment.TrackFragment.TrackFragmentHeader.TrackId).ToList();
                            if (fragmentsFromSameTrack.Count > 1) retryQueue.Remove(fragmentsFromSameTrack.First()); // we only need to store the last 2 fragments per track. Remove everything else + keep in mind that we're about to add a new fragment immediately.
                            retryQueue.Add(fragment); // always store the last 2 fragments sent in case a retry is needed
                        }
                    }
                }
            };
            progress.ProgressChanged += progressChangedHandler;
            // start pushing until we're done sending everything but return immediately from this method.
#if NETFX_CORE
            var sendTask = pushClient.SendBoxesAsync(outputConsumer).AsTask(cts.Token, progress);
#else
            var sendTask = pushClient.SendBoxesAsync(outputConsumer, cts.Token, progress);
#endif

            return RunAndRetryAsync(sendTask, error =>
            {
                lock (syncLock)
                {
                    var args = new PushRetryEventArgs(error.Message);
                    if (PushRetry != null) PushRetry(this, args);
                    if (args.Cancel) throw new OperationCanceledException();

                    // get a copy of last 2 successfully sent fragments + any that were in the process of sending.
                    var fragmentsToResend = retryQueue.ToList();
                    var queuedFragments = pushedFragments.Except(fragmentsToResend).ToList();
                    this.pushedFragments.Clear();
                    this.pushClient.Dispose();
                    this.pushClient = new PushClient();
                    var retryToken = new object();

                    // resend the header boxes
                    pushClient.PushBoxes(new IBox[] { header.FileType, header.LiveServerManifest, header.Movie }, retryToken);
                    // resend the last 2 fragments
                    foreach (var fragment in fragmentsToResend)
                    {
                        pushedFragments.Enqueue(fragment);
                        pushClient.PushBoxes(new IBox[] { fragment.MovieFragment, fragment.MediaData }, retryToken);
                    }
                    // send any that were in the queue
                    foreach (var fragment in queuedFragments)
                    {
                        pushedFragments.Enqueue(fragment);
                        pushClient.PushBoxes(new IBox[] { fragment.MovieFragment, fragment.MediaData }, null);
                    }
                }

#if NETFX_CORE
                return pushClient.SendBoxesAsync(outputConsumer).AsTask(cts.Token, progress);
#else
                return pushClient.SendBoxesAsync(outputConsumer, cts.Token, progress);
#endif
            }, MaxRetries, RetryDelay).ContinueWith(t =>
            {
                progress.ProgressChanged -= progressChangedHandler;
                if (t.IsCanceled) throw new OperationCanceledException();
                else if (t.IsFaulted)
                {
                    if (PushFailed != null) PushFailed(this, new PushFailureEventArgs(t.Exception.GetBaseException().Message));
                    throw t.Exception.GetBaseException();
                }
            });
        }

        static async Task RunAndRetryAsync(Task action, Func<Exception, Task> retry, int? maxRetries, TimeSpan? retryDelay)
        {
            int retryCount = 0;
            bool success = false;
            do
            {
                try
                {
                    await action;
                    success = true;
                }
                catch (OperationCanceledException) { throw; }
                catch (Exception ex)
                {
                    retryCount++;
                    if (retryCount > maxRetries.GetValueOrDefault(int.MaxValue)) throw;
                    action = retry(ex);
                }
                if (!success && retryDelay.HasValue)
                {
#if NETFX_CORE
                    await Task.Delay(retryDelay.Value);
#else
                    await TaskEx.Delay(retryDelay.Value);
#endif
                }
            } while (!success);
        }

#if NETFX_CORE
        /// <summary>
        /// Call to queue a new fragment. Note: this is not thread safe and the PushFragment method should be used instead when calling from a background thread.
        /// </summary>
        /// <param name="fragment">The fragment that needs to be sent to the server.</param>
        /// <returns>Returns an awaitable action that returns when the fragment has been successfully queued.</returns>
        public IAsyncAction PushFragmentAsync(PiffFragment fragment)
        {
            return AsyncInfo.Run(c => PushFragmentAsync(fragment, c));
        }

        async Task PushFragmentAsync(PiffFragment fragment, CancellationToken c)
#else
        /// <summary>
        /// Call to queue a new fragment. Note: this is not thread safe and the PushFragment method should be used instead when calling from a background thread.
        /// </summary>
        /// <param name="fragment">The fragment that needs to be sent to the server.</param>
        /// <param name="c">A cancellation token that can be used to cancel the operation.</param>
        /// <returns>Returns an awaitable action that returns when the fragment has been successfully queued.</returns>
        public async Task PushFragmentAsync(PiffFragment fragment, CancellationToken c)
#endif
        {
            if (retryTaskCompletionSource != null) await retryTaskCompletionSource.Task;
            cts.Token.ThrowIfCancellationRequested();

            fragmentIndex++;
            fragment.MovieFragment.MovieFragmentHeader.SequenceNumber = fragmentIndex; // force sequence number order.
            pushedFragments.Enqueue(fragment);
            if (FragmentQueued != null) FragmentQueued(this, new FragmentDeliveryEventArgs(fragment));

            using (var cts2 = CancellationTokenSource.CreateLinkedTokenSource(c, cts.Token))
            {
#if NETFX_CORE
                await pushClient.PushBoxesAsync(new IBox[] { fragment.MovieFragment, fragment.MediaData }, null).AsTask(cts2.Token);
#else
                await pushClient.PushBoxesAsync(new IBox[] { fragment.MovieFragment, fragment.MediaData }, null, cts2.Token);
#endif
            }
        }

        /// <summary>
        /// Call to queue a new fragment. Note: this is thread safe and should be used instead of PushFragmentAsync when calling from a background thread.
        /// </summary>
        /// <param name="fragment">The fragment that needs to be sent to the server.</param>
        public void PushFragment(PiffFragment fragment)
        {
            lock (syncLock)
            {
                fragmentIndex++;
                fragment.MovieFragment.MovieFragmentHeader.SequenceNumber = fragmentIndex; // force sequence number order.
                pushedFragments.Enqueue(fragment);
                if (FragmentQueued != null) FragmentQueued(this, new FragmentDeliveryEventArgs(fragment));
                pushClient.PushBoxes(new IBox[] { fragment.MovieFragment, fragment.MediaData }, null);
            }
        }

#if NETFX_CORE
        /// <summary>
        /// Completes the session by queuing the PIFF footer. Note: this is not thread safe and the Complete method should be used instead when calling from a background thread.
        /// </summary>
        /// <param name="footer">The PIFF footer (mfra box)</param>
        /// <returns>An awaitable action that returns when the data has been queued.</returns>
        public IAsyncAction CompleteAsync(PiffFooter footer)
        {
            return AsyncInfo.Run(c => CompleteAsync(footer, c));
        }

        async Task CompleteAsync(PiffFooter footer, CancellationToken c)
#else
        /// <summary>
        /// Completes the session by queuing the PIFF footer. Note: this is not thread safe and the Complete method should be used instead when calling from a background thread.
        /// </summary>
        /// <param name="footer">The PIFF footer (mfra box)</param>
        /// <param name="c">A cancellation token that can be used to cancel the operation.</param>
        /// <returns>An awaitable action that returns when the data has been queued.</returns>
        public async Task CompleteAsync(PiffFooter footer, CancellationToken c)
#endif
        {
            cts.Token.ThrowIfCancellationRequested();
#if NETFX_CORE
            await pushClient.PushBoxAsync(footer.MovieFragmentRandomAccess, null).AsTask(cts.Token);
#else
            await pushClient.PushBoxAsync(footer.MovieFragmentRandomAccess, null, cts.Token);
#endif
            pushClient.PushComplete();
        }

        /// <summary>
        /// Completes the session by queuing the PIFF footer. Note: this is thread safe and should be used instead of CompleteAsync when calling from a background thread.
        /// </summary>
        /// <param name="footer">The PIFF footer (mfra box)</param>
        public void Complete(PiffFooter footer)
        {
            lock (syncLock)
            {
                if (pushClient != null) // make sure we successfully started.
                {
                    pushClient.PushBox(footer.MovieFragmentRandomAccess, null);
                    pushClient.PushComplete();
                }
            }
        }

#if NETFX_CORE
        /// <summary>
        /// Finalizes the session by waiting for all data to be successfully sent. Call after Complete or CompleteAsync.
        /// </summary>
        /// <returns>An awaitable action that will not return until all data has successfully been sent.</returns>
        public IAsyncAction FinalizeAsync()
        {
            return AsyncInfo.Run(c => FinalizeAsync(c));
        }

        async Task FinalizeAsync(CancellationToken c)
#else
        /// <summary>
        /// Finalizes the session by waiting for all data to be successfully sent. Call after Complete or CompleteAsync.
        /// </summary>
        /// <param name="c">A cancellation token that can be used to cancel the operation.</param>
        /// <returns>An awaitable action that will not return until all data has successfully been sent.</returns>
        public async Task FinalizeAsync(CancellationToken c)
#endif
        {
            if (pushTask != null)
            {
                await pushTask;
                pushTask = null;
            }

            this.header = null;
        }

        /// <summary>
        /// Cancels the current session as soon as possible.
        /// </summary>
        public void Cancel()
        {
            cts.Cancel();
        }

        /// <inheritdoc />
        public void Dispose()
        {
            this.cts.Dispose();
            this.cts = null;
            outputConsumer = null;
        }
    }

    /// <summary>
    /// Event args to provide additional information about a push retry event.
    /// </summary>
#if NETFX_CORE
    public sealed class PushRetryEventArgs
#else
    public sealed class PushRetryEventArgs : EventArgs
#endif
    {
        internal PushRetryEventArgs(string error)
        {
            Error = error;
        }

        /// <summary>
        /// Gets the error that necessitated the retry.
        /// </summary>
        public string Error { get; private set; }

        /// <summary>
        /// Gets or sets whether the retry should be allowed to proceed. If false, a push failure will occur next.
        /// </summary>
        public bool Cancel { get; set; }
    }

    /// <summary>
    /// Event args to provide additional information about a push failure event.
    /// </summary>
#if NETFX_CORE
    public sealed class PushFailureEventArgs
#else
    public sealed class PushFailureEventArgs : EventArgs
#endif
    {
        internal PushFailureEventArgs(string error)
        {
            Error = error;
        }

        /// <summary>
        /// Gets the error that cause the failure. This is the error associated with the final retry.
        /// </summary>
        public string Error { get; private set; }
    }

    /// <summary>
    /// Event args to provide additional information when a box has been successfully pushed to the queue.
    /// </summary>
#if NETFX_CORE
    public sealed class BoxPushedEventArgs
#else
    public sealed class BoxPushedEventArgs : EventArgs
#endif
    {
        internal BoxPushedEventArgs(IBox box)
        {
            Box = box;
        }

        /// <summary>
        /// Gets the box pushed into the queue.
        /// </summary>
        public IBox Box { get; private set; }
    }

    /// <summary>
    /// Event args to provide additional information about a PIFF fragment that is ready for delivery to the ingest service.
    /// </summary>
#if NETFX_CORE
    public sealed class FragmentDeliveryEventArgs
#else
    public sealed class FragmentDeliveryEventArgs : EventArgs
#endif
    {
        internal FragmentDeliveryEventArgs(PiffFragment fragment)
        {
            Fragment = fragment;
            Purpose = FragmentDeliveryPurpose.Normal; ;
        }

        internal FragmentDeliveryEventArgs(PiffFragment fragment, FragmentDeliveryPurpose purpose)
        {
            Fragment = fragment;
            Purpose = purpose;
        }

        /// <summary>
        /// Gets a model containing the PIFF fragment.
        /// </summary>
        public PiffFragment Fragment { get; private set; }

        /// <summary>
        /// Gets the purpose of the fragment (e.g. retry)
        /// </summary>
        public FragmentDeliveryPurpose Purpose { get; private set; }
    }

    /// <summary>
    /// Provides additional information about the circumstances under which the fragment was sent.
    /// </summary>
    public enum FragmentDeliveryPurpose
    { 
        /// <summary>
        /// The fragment is being sent for the first time.
        /// </summary>
        Normal,
        /// <summary>
        /// Sent during a retry attempt in the event of a connection failure. This implies fragment has already been sent once.
        /// </summary>
        Retry
    }
}
